<?php
/**
 * coreModel
 *
 * 데이터베이스 Model 기반 클래스
 *
 * @package core
 * @author  Platform Team
 */

class coreModel
{
    /**
     * 캐시 루트 디렉토리 명
     * @var string
     */
    const DMC_DIR = 'dmc';

    /**
     * 커넥션 객체 풀
     * @var array(
     *  '인스턴스키' => coreModelDbDriver[드라이버],
     *  ...
     * )
     */
    private static $aDbDriver;

    /**
     * 액티브 레코드 드라이버 객체 풀
     * @var array(
     *  '드라이버명' => coreModelActiverecordDriver[드라이버명],
     *  ...
     * )
     */
    private static $aArDriver;

    /**
     * 커넥션 객체 - master
     *
     * !!NOTICE!!
     * 실제로 할당되는 객체는 coreModelDbDriver[드라이버]
     *
     * @var coreModelDbDriverCommon
     */
    protected $dbDriver;

    /**
     * 커넥션 객체 - slave
     *
     * !!NOTICE!!
     * 실제로 할당되는 객체는 coreModelDbDriver[드라이버]
     *
     * @var coreModelDbDriverCommon
     */
    protected $dbDriverSlave;

    /**
     * 캐시 객체
     *
     * !!NOTICE!!
     * 실제로 반환되는 객체는 coreModelCacheDriver[드라이버명]
     *
     * @var coreModelCacheDriverCommon
     */
    protected $dbCache;

    /**
     * 캐시 객체 - 지워질 예정
     *
     * !!NOTICE!!
     * 실제로 반환되는 객체는 coreModelCacheDriver[드라이버명]
     *
     * @var coreModelCacheDriverCommon
     * @deprecated
     */
    protected $dbCacheConfig;

    /**
     * 액티브 레코드 패턴 객체
     * @var coreModelActiverecord
     */
    protected $helper;

    /**
     * 캐시 네임스페이스(일반적으로 클래스명이 할당됨)
     * @var string
     */
    private $sCacheNsp;

    /**
     * 슬래이브 DB 사용 여부
     * @var boolean
     */
    private $isReplicate;

    /**
     * 트랜잭션을 실행중인지 여부
     * @var boolean
     */
    private $isTransacting;

    /**
     * EXEC 타입 쿼리 비활성화 여부
     * @var boolean
     */
    private $isExecDisabled = false;

    /**
     * 클래스명
     * @var string
     */
    private $sClassName;

    /**
     * 암호화 키
     * @var string
     */
    protected $sEncryptKey = 'DEFAULT_KEY';

    /**
     * 암호화 벡터
     * @var string
     */
    protected $sEncryptIV = 'DEFAULT_IV';

    /**
     * 모델 생성자
     *
     * @param array $aDSN             DSN 정보 배열
     * @param string $sCacheName      캐시 네임스페이스
     * @param array $aCacheConfig     캐시 설정
     */
    public function __construct($aDSN, $sCacheNsp=null, $aCacheConfig=array())
    {
        $this->sClassName = get_class($this);
        $this->isTransacting = false;

        if (empty($sCacheNsp)) {
            $this->sCacheNsp = $this->sClassName;
        } else {
            $this->sCacheNsp = $sCacheNsp;
        }

        $sInsKey = isset($aDSN['instance_key']) ? $aDSN['instance_key'] : serialize($aDSN);
        $sInsKeySlave = $sInsKey . 'slave';

        $this->isExecDisabled = $aDSN['is_exec_disabled'];

        $this->isReplicate = $this->_isReplicate($aDSN);

        $this->dbDriver = $this->_createDbConnector($aDSN, $sInsKey, $this->isReplicate);

        if ($this->isReplicate === true) {
            $this->dbDriverSlave = $this->_createDbConnector($aDSN, $sInsKeySlave, $this->isReplicate, false);
        } else {
            $this->dbDriverSlave = false;
        }

        $ArDriver = $this->_createArDriver($aDSN['driver'], $this->dbDriver, $this->dbDriverSlave);
        $this->helper = new coreModelActiverecord($ArDriver, array($this->dbDriver, $this->dbDriverSlave), true);

        $this->helper->setEncryptKey($this->sEncryptKey)->setEncryptIV($this->sEncryptIV);

        $this->dbCache = $this->_createCacheDriver($aCacheConfig);
        $this->dbCacheConfig = $this->dbCache;

        if (FW_DEBUG_ENABLED === true) {
            $this->dbDriver->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);

            if ($this->dbDriverSlave !== false) {
                $this->dbDriverSlave->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
            }
        }

        $this->__initBefore();
        $this->init();
    }

    /**
     * 소멸자
     *
     * !!NOTICE!!
     * m 함수등을 이용한 객체 생성을 통하지 않고,
     * 직접 생성하여 지역변수로 생성된 객체의 경우 유효 범위를 벗어나도
     * static 멤버 변수가 살아있기 때문에 이후에 동작하는 로직에서 이상이 발생할 수 있어
     * 소멸자를 이용하여 static 멤버 변수를 명시적으로 제거하도록 한다.
     *
     * 주로 단위테스트에서 이러한 예외사용을 하기도 한다.
     */
    public function __destruct()
    {
        self::$aArDriver = null;
        self::$aDbDriver = null;
    }

    /**
     * 초기화 템플릿 메소드 실행전 호출
     */
    protected function __initBefore() {}

    /**
     * 초기화 템플릿 메소드
     */
    protected function init() {}

    /**
     * 쿼리 실행
     *
     * !!NOTICE!!
     * 두번째 인자인 쿼리타입에 null 을 입력하면 캐시를 갱신하지 않는다.
     * 예를 들어 set search_path to 같은 쿼리문장은 캐시 데이터에 영향을 주는 쿼리문이 아니므로,
     * 캐시를 갱신할 필요가 없다.
     * 이처럼 캐시 갱신이 되지 않도록 지정할 필요가 있다면 두번째 인자에 null 혹은 빈문자열을 부여하자.
     *
     * @param string $sSql             SQL문
     * @param string $sType            반환타입 (기본값 : rows (exec|row|rows))
     * @param int $iExpire             캐시 유효시간 (기본값 : false) - 캐시 사용여부가 활성화 되면 기본값으로 1년이 할당됨.
     * @return mixed | false
     */
    protected function query($sSql, $sType=FW_MODEL_RESULT_ROWS, $iExpire=31536000)
    {
        $sSql = trim($sSql);
        $sType = strtolower($sType);
        $sCacheKey = $sSql . $sType . $iExpire;

        $sSql .= T_dbQueryAppend($this->sClassName);

        $isUseCache = $this->_isUseCache();
        $sCacheNsp = $this->sCacheNsp;

        if (FW_DEBUG_ENABLED === true) {
            coreDebugDb::start();
            coreDebugDb::setModelLog(get_class($this));
            coreDebugDb::setQueryLog($sSql);
        }

        // exec 타입의 쿼리
        if (strcasecmp($sType, FW_MODEL_RESULT_EXEC) == 0 || $this->helper->isSelect($sSql) === false) {

            if ($this->isExecDisabled === true) {
                if ($sType != FW_MODEL_RESULT_EXEC_DISABLED_PASS) {
                    T_dbQueryExecDisabled();
                    return false;
                }
            }

            // 캐시 만료
            if ($isUseCache && $sType) {
                $this->dbCache->expire($sCacheNsp);
            }

            $mResult = $this->dbDriver->exec($sSql);

            if (FW_DEBUG_ENABLED === true) {
                coreDebugDb::end();
            }

            return $mResult;

        }

        if ($isUseCache) {

            $aCacheData = $this->dbCache->read($sCacheNsp, $sCacheKey);

            if ($aCacheData !== false) {

                if (FW_DEBUG_ENABLED === true) {
                    // 타입이 빈값으로 들어오는 경우는 캐싱여부에 관계없는 쿼리라 판단해야 한다.
                    coreDebugDb::setCacheLog('use', $isUseCache && $sType);
                    coreDebugDb::setCacheLog('created-time', $aCacheData['itime']);
                    coreDebugDb::setCacheLog('lifetime', $aCacheData['expire']);
                    coreDebugDb::setCacheLog('cache-path', $aCacheData['identity']);
                    coreDebugDb::end();
                }

                return $aCacheData['data'];
            }

        }

        if ($this->dbDriverSlave !== false) {
            if ($this->isTransacting === true) {
                $mResult = $this->dbDriver->query($sSql, $sType);
            } else {
                $mResult = $this->dbDriverSlave->query($sSql, $sType);
            }
        } else {
            $mResult = $this->dbDriver->query($sSql, $sType);
        }

        if ($isUseCache) {

            $this->dbCache->write($sCacheNsp, $sCacheKey, $mResult, $iExpire);

        }

        if (FW_DEBUG_ENABLED === true) {
            coreDebugDb::end();
        }

        return $mResult;
    }

    /**
     * Last Insert ID 반환
     *
     * @param string $sName
     * 일부 데이터베이스에서는 값을 반드시 지정해야  정상적으로 반환됩니다.
     * MySQL은 일반적으로 값을 지정하지 않아도 자동 반환됩니다.
     * PGSQL은 일반적으로 시퀀스 이름을 지정해야 반환됩니다.
     *
     * @return mixed
     * string representing the row ID of the last row that was inserted into the database
     * string representing the last value retrieved from the specified sequence object
     * If the PDO driver does not support this capability, PDO::lastInsertId() triggers an IM001 SQLSTATE
     */
    public function lastInsertId($sName=null)
    {
        return $this->dbDriver->lastInsertId($sName);
    }

    /**
     * 캐시 초기화 - 지워질 예정
     *
     * @deprecated
     */
    public function setCacheClear($bDmcAll=false)
    {
        //Empty
    }

    /**
     * 이스케입 스트링
     *
     * @return string
     */
    public function escape($sSql)
    {
        return $this->dbDriver->escapeString($sSql);
    }

    /**
     * 트랜잭션 - begin
     *
     * @return boolean || null
     */
    public function begin()
    {
        if ($this->isTransacting === true) {
            return;
        }

        $this->isTransacting = true;

        return $this->dbDriver->beginTransaction();
    }

    /**
     * 트랜잭션 - rollback
     *
     * @return boolean || null
     */
    public function rollback()
    {
        if ($this->isTransacting !== true) {
            return;
        }

        $this->isTransacting = false;

        return $this->dbDriver->rollBack();
    }

    /**
     * 트랜잭션 - commit
     *
     * @return boolean || null
     */
    public function commit()
    {
        if ($this->isTransacting !== true) {
            return;
        }

        $this->isTransacting = false;

        return $this->dbDriver->commit();
    }

    /**
     * 캐시 사용여부 판단
     *
     * @return boolean
     */
    private function _isUseCache()
    {
        return $this->dbCache->useCache();
    }

    /**
     * 슬래이브 DB 사용여부 판단
     *
     * @return boolean
     */
    private function _isReplicate($aDSN)
    {
        return (isset($aDSN['master']) && isset($aDSN['slave'])) ? true : false;
    }

    /**
     * 캐시 기본 설정값 세팅
     *
     * @param array $aConfig
     * @return array
     */
    private function _defaultCacheConfig($aConfig)
    {
        // 유니크 아이디
        // 사용자 계정별로 데이터베이스가 존재하는 솔루션이라면 이곳에 아이디를 지정하길 권장한다.
        if (!isset($aConfig['Uid'])) {
            $aConfig['Uid'] = 'unknownuid_';
        }

        // 캐시 드라이버 타입 - File, Memcache
        if (!isset($aConfig['CacheManage'])) {
            $aConfig['CacheManage'] = 'File';
        }

        // 캐시 사용 여부 - 기본 비활성화
        if (!isset($aConfig['UseCache'])) {
            $aConfig['UseCache'] = false;
        }

        // 캐시 만료 시간 - 1년
        if (!isset($aConfig['ExpireTime'])) {
            $aConfig['ExpireTime'] = 31536000;
        }

        // 캐시 기본 루트 디렉토리
        if (!isset($aConfig['DefaultPath'])) {
            $aConfig['DefaultPath'] = FW_DIR_CACHE . DS . self::DMC_DIR;
        }

        // 캐시 디렉토리 분산 뎁스 (최대 3)
        if (!isset($aConfig['DirectoryVariance'])) {
            $aConfig['DirectoryVariance'] = 2;
        }

        // 멤캐시 호스트 주소 - 캐시 드라이버가 Memcache 일 경우 유효
        if (!isset($aConfig['MemcacheHost'])) {
            $aConfig['MemcacheHost'] = 'localhost';
        }

        // 멤캐시 포트 주소 - 캐시 드라이버가 Memcache 일 경우 유효
        if (!isset($aConfig['MemcachePort'])) {
            $aConfig['MemcachePort'] = 11211;
        }

        return $aConfig;
    }

    /**
     * DB 커넥션 객체 생성
     *
     * 실패시 fasle 반환
     *
     * !!NOTICE!!
     * 실제로 반환되는 객체는 coreModelDbDriver[드라이버명]
     *
     * @param array $aDSN
     * @param string $sInsKey
     * @param boolean $isReplicate
     * @param boolean $isMaster
     * @return coreModelDbDriverCommon
     */
    private function _createDbConnector($aDSN, $sInsKey, $isReplicate, $isMaster=true)
    {
        if ($aDSN['none_connect_pool'] !== true && isset(self::$aDbDriver[$sInsKey])) {
            return self::$aDbDriver[$sInsKey];
        }

        $sClass = 'coreModelDbDriver' . ucfirst(strtolower($aDSN['driver']));
        $aParam = array();

        if ($isReplicate === false) {

            $aParam['host'] = $aDSN['host'];
            $aParam['port'] = $aDSN['port'];
            $aParam['dbname'] = $aDSN['dbname'];
            $aParam['id'] = $aDSN['id'];
            $aParam['passwd'] = $aDSN['passwd'];
            $aParam['options'] = !empty($aDSN['options']) ? $aDSN['options'] : array();

        } else {

            if ($isMaster === false) {
                $aParam['host'] = $aDSN['slave']['host'];
                $aParam['port'] = $aDSN['slave']['port'];
                $aParam['dbname'] = $aDSN['slave']['dbname'];
                $aParam['id'] = $aDSN['slave']['id'];
                $aParam['passwd'] = $aDSN['slave']['passwd'];
                $aParam['options'] = !empty($aDSN['slave']['options']) ? $aDSN['slave']['options'] : array();
            } else {
                $aParam['host'] = $aDSN['master']['host'];
                $aParam['port'] = $aDSN['master']['port'];
                $aParam['dbname'] = $aDSN['master']['dbname'];
                $aParam['id'] = $aDSN['master']['id'];
                $aParam['passwd'] = $aDSN['master']['passwd'];
                $aParam['options'] = !empty($aDSN['master']['options']) ? $aDSN['master']['options'] : array();
            }

        }

        $sDsnParam = sprintf('%s:host=%s;port=%s;dbname=%s', $aDSN['driver'], $aParam['host'], $aParam['port'], $aParam['dbname']);

        $Conn = new $sClass($sDsnParam, $aParam['id'], $aParam['passwd'], $aParam['options']);

        if ($Conn) {
            self::$aDbDriver[$sInsKey] = $Conn;
        }

        return $Conn ? $Conn : false;
    }

    /**
     * 액티브 레코드 패턴 객체 생성
     *
     * @param string $sDriver
     * @param $DBDriver
     * @param $DBDriverSlave
     * @return coreModelActiverecordDriver[드라이버명]
     */
    private function _createArDriver($sDriver, $DBDriver, $DBDriverSlave)
    {
        if (isset(self::$aArDriver[$sDriver]) === true) {
            return self::$aArDriver[$sDriver];
        }

        $sClass = 'coreModelActiverecordDriver' . ucfirst(strtolower($sDriver));

        $ArDriver = new $sClass;

        self::$aArDriver[$sDriver] = $ArDriver;

        return $ArDriver;
    }

    /**
     * 캐시 드라이버 설정
     *
     * !!NOTICE!!
     * 실제로 반환되는 객체는 coreModelCacheDriver[드라이버명]
     *
     * @param array $aConfig
     * @return coreModelCacheDriverCommon
     */
    private function _createCacheDriver($aConfig)
    {
        $aConfig = $this->_defaultCacheConfig($aConfig);

        $sClass = 'coreModelCacheDriver' . ucfirst(strtolower($aConfig['CacheManage']));

        return new $sClass($aConfig);
    }
}